<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Lesson 01 - Echo (多连接版)</title>
  <style>
    body { font-family: sans-serif; margin: 24px; }
    #log { height: 400px; overflow: auto; border: 1px solid #ddd; padding: 8px; background: #fafafa; }
    input, button { padding: 6px 10px; margin: 2px; }
    .connection-panel { 
      border: 1px solid #2196F3; 
      margin: 10px 0; 
      padding: 10px; 
      border-radius: 5px; 
      background: #f3f9ff;
      border-left: 4px solid #2196F3;
    }
    .connection-header { 
      font-weight: bold; 
      margin-bottom: 8px; 
      display: flex; 
      align-items: center; 
      gap: 10px;
      color: #1976D2;
    }
    .connection-id { 
      display: inline-block; 
      padding: 2px 8px; 
      border-radius: 3px; 
      color: white; 
      font-size: 12px;
    }
    .connection-status { 
      font-size: 14px; 
      padding: 2px 6px; 
      border-radius: 3px;
    }
    .status-connected { background: #4CAF50; color: white; }
    .status-connecting { background: #FF9800; color: white; }
    .status-disconnected { background: #f44336; color: white; }
    .log-entry { margin: 2px 0; padding: 2px 5px; border-radius: 3px; }
    .global-controls { 
      background: #e3f2fd; 
      padding: 10px; 
      border-radius: 5px; 
      margin-bottom: 15px;
      border-left: 4px solid #2196F3;
    }
    .advanced-feature { 
      background: #e8f5e8; 
      padding: 2px 6px; 
      border-radius: 3px; 
      font-size: 11px; 
      color: #2e7d32;
    }
  </style>
</head>
<body>
<h3>WebSocket Echo Answers (多连接测试)</h3>

<div class="global-controls">
  <h4 style="margin-top: 0; color: #1976D2;">全局控制</h4>
  <label>URL: </label>
  <input id="url" size="40" value="ws://localhost:8101/ws/echo"/>
  <button id="newConnection">新建连接</button>
  <button id="closeAll">关闭所有连接</button>
  <button id="clear">清空日志</button>
  <span style="margin-left: 10px;">
    <a href="/metrics" target="_blank" style="color: #1976D2;">📊 查看指标</a>
  </span>
</div>

<div id="connections"></div>

<div>
  <h4>消息日志 <span class="advanced-feature">支持功能测试</span></h4>
  <div id="log"></div>
</div>
  <hr/>

  <script>
    let connections = new Map(); // 存储所有连接
    let nextConnectionId = 1;
    const logDiv = document.getElementById('log');
    const connectionsDiv = document.getElementById('connections');
    
    // 预定义的颜色数组（使用蓝色系为主）
    const colors = [
      '#2196F3', '#1976D2', '#0D47A1', '#03A9F4', 
      '#00BCD4', '#009688', '#4CAF50', '#8BC34A',
      '#FF9800', '#F57C00', '#FF5722', '#9C27B0'
    ];
    
    function getTimestamp() {
      return new Date().toLocaleTimeString();
    }
    
    function log(message, connectionId = null, type = 'info') {
      const p = document.createElement('div');
      p.className = 'log-entry';
      
      let prefix = `[${getTimestamp()}]`;
      if (connectionId) {
        const conn = connections.get(connectionId);
        if (conn) {
          prefix += ` [连接${connectionId}]`;
          p.style.borderLeft = `3px solid ${conn.color}`;
          p.style.paddingLeft = '8px';
        }
      }
      
      p.textContent = `${prefix} ${message}`;
      
      // 根据消息类型设置样式
      if (type === 'error') {
        p.style.color = '#d32f2f';
      } else if (type === 'success') {
        p.style.color = '#1976D2';
      } else if (type === 'warning') {
        p.style.color = '#f57c00';
      } else if (type === 'rate-limit') {
        p.style.color = '#9c27b0';
        p.style.fontWeight = 'bold';
      }
      
      logDiv.appendChild(p);
      logDiv.scrollTop = logDiv.scrollHeight;
    }
    
    function createConnection() {
      const url = document.getElementById('url').value;
      const connectionId = nextConnectionId++;
      const color = colors[(connectionId - 1) % colors.length];
      
      const ws = new WebSocket(url);
      const connection = {
        id: connectionId,
        ws: ws,
        color: color,
        status: 'connecting',
        autoPingTimer: null,
        isIdleTesting: false
      };
      
      connections.set(connectionId, connection);
      
      // WebSocket事件处理
      ws.onopen = () => {
        connection.status = 'connected';
        updateConnectionPanel(connectionId);
        startAutoPing(connectionId);
        log('连接已建立 (支持所有功能，自动PING已启动)', connectionId, 'success');
      };
      
      ws.onmessage = (e) => {
        const data = e.data;
        if (data === 'pong') {
          log('收到PONG响应', connectionId, 'success');
        } else if (data === 'rate limited') {
          log('触发速率限制保护', connectionId, 'rate-limit');
        } else {
          log(`收到消息: ${data}`, connectionId);
        }
      };
      
      ws.onerror = (e) => {
        log('连接发生错误', connectionId, 'error');
      };
      
      ws.onclose = (e) => {
        connection.status = 'disconnected';
        connection.isIdleTesting = false; // 重置空闲测试状态
        stopAutoPing(connectionId); // 停止自动PING
        updateConnectionPanel(connectionId);
        let reason = '';
        if (e.code === 1009) {
          reason = ' (消息过大被拒绝)';
        } else if (e.code === 1000 && e.reason === 'idle timeout') {
          reason = ' (空闲超时)';
        }
        log(`连接已关闭 (code=${e.code}${reason})`, connectionId, 'warning');
      };
      
      createConnectionPanel(connection);
      log(`正在连接到WebSocket服务 ${url}`, connectionId);
    }
    
    function createConnectionPanel(connection) {
      const panel = document.createElement('div');
      panel.className = 'connection-panel';
      panel.id = `connection-${connection.id}`;
      
      panel.innerHTML = `
        <div class="connection-header">
          <span class="connection-id" style="background-color: ${connection.color}">连接${connection.id}</span>
          <span class="connection-status status-connecting" id="status-${connection.id}">连接中</span>
          <div style="margin-left: auto;">
            <button id="reconnect-${connection.id}" onclick="reconnectConnection(${connection.id})" style="display: none; background: #4CAF50; color: white;">重新连接</button>
            <button id="close-${connection.id}" onclick="closeConnection(${connection.id})">关闭连接</button>
            <button id="remove-${connection.id}" onclick="removeConnection(${connection.id})" style="display: none; background: #f44336; color: white;">移除</button>
          </div>
        </div>
        <div style="margin-bottom: 8px;">
          <input id="msg-${connection.id}" placeholder="输入消息" style="width: 200px;">
          <button id="send-${connection.id}" onclick="sendMessage(${connection.id})">发送</button>
          <button id="ping-${connection.id}" onclick="sendPing(${connection.id})">PING</button>
        </div>
        <div id="actions-${connection.id}">
          <button onclick="sendBroadcast(${connection.id})" style="background: #4CAF50; color: white;">广播测试</button>
          <button onclick="sendLargeMessage(${connection.id})" style="background: #FF9800; color: white;">大小限制测试</button>
          <button onclick="testRateLimit(${connection.id})" style="background: #9C27B0; color: white;">速率限制测试</button>
          <button onclick="testIdleTimeout(${connection.id})" style="background: #607D8B; color: white;">空闲测试</button>
        </div>
      `;
      
      connectionsDiv.appendChild(panel);
      
      // 为输入框添加回车事件
      const msgInput = document.getElementById(`msg-${connection.id}`);
      msgInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
          sendMessage(connection.id);
        }
      });
    }
    
    function updateConnectionPanel(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      const statusSpan = document.getElementById(`status-${connectionId}`);
      const reconnectBtn = document.getElementById(`reconnect-${connectionId}`);
      const closeBtn = document.getElementById(`close-${connectionId}`);
      const removeBtn = document.getElementById(`remove-${connectionId}`);
      const msgInput = document.getElementById(`msg-${connectionId}`);
      const sendBtn = document.getElementById(`send-${connectionId}`);
      const pingBtn = document.getElementById(`ping-${connectionId}`);
      const actionsDiv = document.getElementById(`actions-${connectionId}`);
      
      if (!statusSpan) return;
      
      statusSpan.className = 'connection-status';
      
      switch (connection.status) {
        case 'connected':
          statusSpan.textContent = '已连接';
          statusSpan.classList.add('status-connected');
          
          // 显示/隐藏按钮
          reconnectBtn.style.display = 'none';
          closeBtn.style.display = 'inline-block';
          removeBtn.style.display = 'none';
          
          // 启用功能按钮
          msgInput.disabled = false;
          sendBtn.disabled = false;
          pingBtn.disabled = false;
          setActionsEnabled(connectionId, true);
          break;
          
        case 'connecting':
          statusSpan.textContent = '连接中';
          statusSpan.classList.add('status-connecting');
          
          // 显示/隐藏按钮
          reconnectBtn.style.display = 'none';
          closeBtn.style.display = 'inline-block';
          removeBtn.style.display = 'none';
          
          // 禁用功能按钮
          msgInput.disabled = true;
          sendBtn.disabled = true;
          pingBtn.disabled = true;
          setActionsEnabled(connectionId, false);
          break;
          
        case 'disconnected':
          statusSpan.textContent = '已断开';
          statusSpan.classList.add('status-disconnected');
          
          // 显示/隐藏按钮
          reconnectBtn.style.display = 'inline-block';
          closeBtn.style.display = 'none';
          removeBtn.style.display = 'inline-block';
          
          // 禁用功能按钮
          msgInput.disabled = true;
          sendBtn.disabled = true;
          pingBtn.disabled = true;
          setActionsEnabled(connectionId, false);
          break;
      }
    }
    
    function setActionsEnabled(connectionId, enabled) {
      const actionsDiv = document.getElementById(`actions-${connectionId}`);
      if (!actionsDiv) return;
      
      const buttons = actionsDiv.querySelectorAll('button');
      buttons.forEach(btn => {
        btn.disabled = !enabled;
        if (!enabled) {
          btn.style.opacity = '0.5';
          btn.style.cursor = 'not-allowed';
        } else {
          btn.style.opacity = '1';
          btn.style.cursor = 'pointer';
        }
      });
    }
    
    function startAutoPing(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      // 清除已存在的定时器
      if (connection.autoPingTimer) {
        clearInterval(connection.autoPingTimer);
      }
      
      // 启动自动PING定时器（10秒间隔）
      connection.autoPingTimer = setInterval(() => {
        // 检查连接状态和空闲测试状态
        if (connection.ws.readyState === WebSocket.OPEN && 
            !connection.isIdleTesting && 
            connection.status === 'connected') {
          connection.ws.send('ping');
          log('自动发送PING', connectionId, 'info');
        }
      }, 10000); // 10秒间隔
      
      log('自动PING已启动 (每10秒)', connectionId, 'info');
    }
    
    function stopAutoPing(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      if (connection.autoPingTimer) {
        clearInterval(connection.autoPingTimer);
        connection.autoPingTimer = null;
        log('自动PING已停止', connectionId, 'info');
      }
    }
    
    function sendMessage(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法发送消息', connectionId, 'error');
        return;
      }
      
      const msgInput = document.getElementById(`msg-${connectionId}`);
      const message = msgInput.value.trim();
      if (!message) return;
      
      connection.ws.send(message);
      log(`发送消息: ${message}`, connectionId);
      msgInput.value = '';
      
      // 如果在空闲测试状态，发送消息后恢复自动PING
      if (connection.isIdleTesting) {
        connection.isIdleTesting = false;
        log('发送消息后恢复自动PING', connectionId, 'info');
      }
    }
    
    function sendPing(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法发送PING', connectionId, 'error');
        return;
      }
      
      connection.ws.send('ping');
      log('手动发送PING (测试心跳机制)', connectionId);
      
      // 如果在空闲测试状态，手动发送PING后恢复自动PING
      if (connection.isIdleTesting) {
        connection.isIdleTesting = false;
        log('手动PING后恢复自动PING', connectionId, 'info');
      }
    }
    
    function sendBroadcast(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法发送广播', connectionId, 'error');
        return;
      }
      
      const message = `broadcast:来自连接${connectionId}的广播消息 [${getTimestamp()}]`;
      connection.ws.send(message);
      log(`发送广播测试: ${message}`, connectionId, 'success');
    }
    
    function sendLargeMessage(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法测试大小限制', connectionId, 'error');
        return;
      }
      
      // 生成超过1KB的消息
      const largeMessage = 'A'.repeat(1100); // 1100字符，超过1KB
      connection.ws.send(largeMessage);
      log(`发送大消息测试 (${largeMessage.length}字符) - 应该触发大小限制`, connectionId, 'warning');
    }
    
    function testRateLimit(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法测试速率限制', connectionId, 'error');
        return;
      }
      
      log('开始速率限制测试 - 快速发送10条消息', connectionId, 'warning');
      
      // 快速发送10条消息，应该触发速率限制
      for (let i = 1; i <= 10; i++) {
        setTimeout(() => {
          if (connection.ws.readyState === WebSocket.OPEN) {
            connection.ws.send(`速率测试消息 ${i}/10`);
            log(`发送速率测试消息 ${i}/10`, connectionId);
          }
        }, i * 50); // 每50ms发送一条，很快就会超过5条/秒的限制
      }
    }
    
    function testIdleTimeout(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection || connection.ws.readyState !== WebSocket.OPEN) {
        log('连接未就绪，无法测试空闲超时', connectionId, 'error');
        return;
      }
      
      // 标记为空闲测试状态，停止自动PING
      connection.isIdleTesting = true;
      log('空闲超时测试已开始 - 自动PING已暂停', connectionId, 'warning');
      log('此连接将在30秒后自动关闭（如无其他活动）', connectionId, 'warning');
      log('提示：发送任何消息或手动PING可重置超时计时器', connectionId, 'warning');
      
      // 30秒后如果连接还在且仍在空闲测试状态，则恢复自动PING
      setTimeout(() => {
        if (connection.isIdleTesting && connection.ws.readyState === WebSocket.OPEN) {
          connection.isIdleTesting = false;
          log('空闲测试超时，恢复自动PING', connectionId, 'info');
        }
      }, 35000); // 35秒后恢复，给服务器30秒超时留点余量
    }
    
    function reconnectConnection(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      log(`正在重新连接...`, connectionId, 'info');
      
      // 创建新的WebSocket连接
      const url = document.getElementById('url').value;
      const ws = new WebSocket(url);
      
      // 更新连接对象
      connection.ws = ws;
      connection.status = 'connecting';
      updateConnectionPanel(connectionId);
      
      // 重新设置事件处理
      ws.onopen = () => {
        connection.status = 'connected';
        connection.isIdleTesting = false; // 重置空闲测试状态
        updateConnectionPanel(connectionId);
        startAutoPing(connectionId);
        log('重新连接成功 (支持所有功能，自动PING已启动)', connectionId, 'success');
      };
      
      ws.onmessage = (e) => {
        const data = e.data;
        if (data === 'pong') {
          log('收到PONG响应', connectionId, 'success');
        } else if (data === 'rate limited') {
          log('触发速率限制保护', connectionId, 'rate-limit');
        } else {
          log(`收到消息: ${data}`, connectionId);
        }
      };
      
      ws.onerror = (e) => {
        log('重新连接发生错误', connectionId, 'error');
      };
      
      ws.onclose = (e) => {
        connection.status = 'disconnected';
        updateConnectionPanel(connectionId);
        let reason = '';
        if (e.code === 1009) {
          reason = ' (消息过大被拒绝)';
        } else if (e.code === 1000 && e.reason === 'idle timeout') {
          reason = ' (空闲超时)';
        }
        log(`连接已关闭 (code=${e.code}${reason})`, connectionId, 'warning');
      };
    }
    
    function closeConnection(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      if (connection.ws.readyState === WebSocket.OPEN || connection.ws.readyState === WebSocket.CONNECTING) {
        connection.ws.close();
      }
      
      // 停止自动PING
      stopAutoPing(connectionId);
      connection.isIdleTesting = false; // 重置空闲测试状态
      
      // 不移除面板，只更新状态为断开
      connection.status = 'disconnected';
      updateConnectionPanel(connectionId);
      log(`手动关闭连接`, connectionId, 'warning');
    }
    
    function removeConnection(connectionId) {
      const connection = connections.get(connectionId);
      if (!connection) return;
      
      // 如果连接还活跃，先关闭它
      if (connection.ws.readyState === WebSocket.OPEN || connection.ws.readyState === WebSocket.CONNECTING) {
        connection.ws.close();
      }
      
      // 停止自动PING
      stopAutoPing(connectionId);
      
      // 移除连接面板
      const panel = document.getElementById(`connection-${connectionId}`);
      if (panel) {
        panel.remove();
      }
      
      connections.delete(connectionId);
      log(`连接已移除`, connectionId, 'warning');
    }
    
    function closeAllConnections() {
      for (let [id, connection] of connections) {
        if (connection.ws.readyState === WebSocket.OPEN) {
          connection.ws.close();
        }
        // 停止所有自动PING
        stopAutoPing(id);
      }
      connectionsDiv.innerHTML = '';
      connections.clear();
      log('所有连接已关闭', null, 'warning');
    }
    
    // 全局事件处理
    document.getElementById('newConnection').onclick = createConnection;
    document.getElementById('closeAll').onclick = closeAllConnections;
    document.getElementById('clear').onclick = () => { 
      logDiv.innerHTML = ''; 
      log('日志已清空', null, 'info');
    };
    
    // 页面加载时显示欢迎信息
    window.onload = () => {
      log('WebSocket功能测试工具已就绪', null, 'success');
      log('支持功能：跨域验证、消息大小限制、速率限制、空闲超时、广播、指标统计', null, 'info');
      log('点击"新建连接"开始测试所有功能', null, 'info');
    };
  </script>

  <div style="background-color: #e8f4fd; padding: 15px; border-radius: 5px; margin-top: 10px; border-left: 4px solid #2196F3;">
    <h4 style="margin-top: 0; color: #1976D2;">WebSocket 功能演示（多连接）</h4>
    <p><strong>功能说明：</strong>这是包含所有练习功能的完整WebSocket服务，支持多连接测试，展示了生产级的WebSocket应用特性。</p>
    
    <p><strong>全局控制功能：</strong></p>
    <ul style="margin: 8px 0;">
      <li><strong>新建连接：</strong>创建新的WebSocket连接（支持跨域验证、连接计数）</li>
      <li><strong>关闭所有连接：</strong>同时关闭所有活跃连接（触发资源清理）</li>
      <li><strong>清空日志：</strong>清除消息历史记录</li>
      <li><strong>查看指标：</strong>点击📊链接查看实时连接和消息统计</li>
    </ul>
    
    <p><strong>连接管理功能：</strong></p>
    <ul style="margin: 8px 0;">
      <li><strong>关闭连接：</strong>断开连接但保留面板，可以重新连接</li>
      <li><strong>重新连接：</strong>对已断开的连接重新建立连接（绿色按钮）</li>
      <li><strong>移除：</strong>彻底删除连接面板和相关数据（红色按钮）</li>
    </ul>
    
    <p><strong>每个连接的功能测试：</strong></p>
    <ul style="margin: 8px 0;">
      <li><strong>发送/PING：</strong>基本消息发送和心跳测试</li>
      <li><strong>广播测试：</strong>向所有连接发送广播消息（绿色按钮）</li>
      <li><strong>大小限制测试：</strong>发送超大消息测试1KB限制（橙色按钮）</li>
      <li><strong>速率限制测试：</strong>快速发送消息测试5条/秒限制（紫色按钮）</li>
      <li><strong>空闲测试：</strong>测试30秒无活动自动断开，期间暂停自动PING（灰色按钮）</li>
    </ul>
    
    <p><strong>自动PING功能：</strong></p>
    <ul style="margin: 8px 0;">
      <li><strong>自动启动：</strong>连接建立后自动开始每10秒发送PING</li>
      <li><strong>智能暂停：</strong>进行空闲超时测试时自动暂停PING发送</li>
      <li><strong>自动恢复：</strong>发送任何消息或手动PING后恢复自动PING</li>
      <li><strong>连接管理：</strong>连接关闭时自动停止PING定时器</li>
    </ul>
    
    <p><strong>日志显示特点：</strong></p>
    <ul style="margin: 8px 0;">
      <li>每条消息都显示时间戳和连接ID，不同连接用不同颜色标识</li>
      <li>区分消息类型：普通消息、PONG响应、速率限制、连接状态变化</li>
      <li>自动PING日志：区分自动发送和手动发送的PING消息</li>
      <li>特殊标识：速率限制消息用紫色粗体显示</li>
      <li>错误和警告用不同颜色突出显示</li>
    </ul>
    
    <p><strong>建议测试流程：</strong></p>
    <ol style="margin: 8px 0;">
      <li>创建2-3个连接，观察连接状态和自动PING启动</li>
      <li>观察自动PING：等待10秒观察自动PING发送和PONG响应</li>
      <li>测试基本功能：发送消息、手动PING/PONG心跳</li>
      <li>测试广播功能：一个连接发送广播，所有连接接收</li>
      <li>测试保护机制：大小限制、速率限制</li>
      <li><strong>测试空闲超时：</strong>启动空闲测试，观察自动PING暂停</li>
      <li>空闲测试中发送消息，观察自动PING恢复</li>
      <li>测试连接管理：手动关闭连接，观察自动PING停止</li>
      <li><strong>测试重连功能：</strong>重新连接，观察自动PING重新启动</li>
      <li>访问/metrics查看实时统计数据</li>
    </ol>
  </div>
</div>
</body>
</html>


